package redis

import (
	"encoding/json"
	"fmt"
	"github.com/edgexfoundry/edgex-go/internal/app/infrastructure/mock"
	"github.com/edgexfoundry/edgex-go/internal/app/models"
	"github.com/edgexfoundry/edgex-go/internal/app/pkg/controller/http"
	"github.com/edgexfoundry/edgex-go/internal/app/pkg/utils"
	pkgCommon "github.com/edgexfoundry/edgex-go/internal/pkg/common"
	edgexRedis "github.com/edgexfoundry/edgex-go/internal/pkg/infrastructure/redis"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/errors"
	edgexModels "github.com/edgexfoundry/go-mod-core-contracts/v2/models"
	"github.com/gomodule/redigo/redis"
	"github.com/google/uuid"
	"time"
)

const (
	DeviceCollection           = "md|dv"
	DeviceCodeCollection       = DeviceCollection + DBKeySeparator + Code
	DeviceNameSearchCollection = DeviceCollection + DBKeySeparator + common.Name + DBKeySeparator + Search
	DeviceCodeNameCollection   = DeviceCollection + DBKeySeparator + Code + DBKeySeparator + common.Name
	ProductDeviceCollection    = ProductCollection + DBKeySeparator + common.Device
	DeviceLastUpdateTime       = "md|lut"
)

// deviceStoredKey return the device's stored key which combines the collection name and object id
func deviceStoredKey(id string) string {
	return CreateKey(DeviceCollection, id)
}

// deviceIdExists checks whether the device exists by id
func deviceIdExists(conn redis.Conn, id string) (bool, errors.EdgeX) {
	exists, err := objectIdExists(conn, deviceStoredKey(id))
	if err != nil {
		return false, errors.NewCommonEdgeX(errors.KindDatabaseError, "device existence check by id failed", err)
	}
	return exists, nil
}

// getDeviceIdByCode 根据设备标识获取设备key
func getDeviceIdByCode(conn redis.Conn, code string) (string, errors.EdgeX) {
	return getFieldValueByHash(conn, DeviceCodeCollection, CreateKey(DeviceCodeCollection, code))
}

// deviceCodeExists checks whether the device exists by code
func deviceCodeExists(conn redis.Conn, code string) (bool, errors.EdgeX) {

	exists, err := objectNameExists(conn, DeviceCodeCollection, CreateKey(DeviceCodeCollection, code))
	if err != nil {
		return false, errors.NewCommonEdgeX(errors.KindDatabaseError, "device existence check by id failed", err)
	}
	return exists, nil
}

// 名称产品组合查询
func devicesGetByNameAndProduct(conn redis.Conn, name, productId string, offset, limit int) (devices []models.Device, totalCount uint32, err errors.EdgeX) {

	ids, err := fieldsByZSet(conn, DeviceCodeNameCollection)
	if err != nil {
		return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
	}

	keys := utils.FilterKeysContainsWithoutPrefix(ids, name, DeviceCodeNameCollection)

	_ = checkKeyValid(conn, &keys)

	if productId != "" {
		ks, err := getDeviceKeyByProductId(conn, productId)
		if err != nil {
			return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
		}
		keys = pkgCommon.FindCommonStrings(ks, keys)
	}

	end := offset + limit
	length := len(keys)

	if offset >= length {
		return
	}
	if end >= length {
		end = length
	}

	keys = keys[offset:end]
	for _, key := range keys {
		var device models.Device
		err = getObjectById(conn, key, &device)
		if err != nil {
			continue
		}
		devices = append(devices, device)
	}

	totalCount = uint32(len(keys))

	devices, _ = fulfillProductInfoInDevices(conn, devices)
	return devices, totalCount, nil
}

// checkKeyValid 判断是不是有效的key
func checkKeyValid(conn redis.Conn, keys *[]string) errors.EdgeX {

	var validKeys []string
	for _, key := range *keys {
		s, _ := getFieldValueByHash(conn, DeviceNameSearchCollection, key)
		if s != "" {
			validKeys = append(validKeys, s)
		}
	}
	*keys = validKeys
	return nil
}

// getDeviceKeyByProductId 根据产品id获取设备keys
func getDeviceKeyByProductId(conn redis.Conn, productId string) ([]string, errors.EdgeX) {
	productDeviceKey := CreateKey(ProductDeviceCollection, productId)
	return fieldsByZSet(conn, productDeviceKey)
}

// devicesGetByProduct 根据产品id获取设备
func devicesGetByProduct(conn redis.Conn, productId string, offset, limit int) (devices []models.Device, totalCount uint32, err errors.EdgeX) {
	storeKey := CreateKey(ProductDeviceCollection, productId)
	objects, totalCount, edgeXerr := getObjectsBySomeRangeWithTotalCount(conn, ZREVRANGE, storeKey, offset, limit)
	if edgeXerr != nil {
		return devices, totalCount, errors.NewCommonEdgeXWrapper(edgeXerr)
	}

	devices = make([]models.Device, len(objects))
	for i, in := range objects {
		dp := models.Device{}
		err := json.Unmarshal(in, &dp)
		if err != nil {
			return []models.Device{}, totalCount, errors.NewCommonEdgeX(errors.KindDatabaseError, http.DataBaseQueryError, err)
		}
		devices[i] = dp
	}
	devices, _ = fulfillProductInfoInDevices(conn, devices)
	return devices, totalCount, nil
}

// devicesGetByLabels 根据label查询设备，暂时labels为空
func devicesGetByLabels(conn redis.Conn, labels []string, offset, limit int) (devices []models.Device, totalCount uint32, err errors.EdgeX) {
	objects, totalCount, edgeXerr := getObjectsByLabelsAndSomeRangeWithTotalCount(conn, ZREVRANGE, DeviceCollection, labels, offset, limit)
	if edgeXerr != nil {
		return devices, totalCount, errors.NewCommonEdgeXWrapper(edgeXerr)
	}

	devices = make([]models.Device, len(objects))
	for i, in := range objects {
		dp := models.Device{}
		err := json.Unmarshal(in, &dp)
		if err != nil {
			return []models.Device{}, totalCount, errors.NewCommonEdgeX(errors.KindDatabaseError, http.DataBaseQueryError, err)
		}
		devices[i] = dp
	}
	devices, _ = fulfillProductInfoInDevices(conn, devices)
	return devices, totalCount, nil
}

// deviceAdd 设备添加
func deviceAdd(conn redis.Conn, device models.Device) (d models.Device, err errors.EdgeX) {

	if device.Code == "" {
		return d, errors.NewCommonEdgeX(errors.KindDuplicateName, http.DeviceCodeNotNull, nil)
	}

	exists, err := deviceCodeExists(conn, device.Code)
	if err != nil {
		return d, errors.NewCommonEdgeXWrapper(err)
	} else if exists {
		return d, errors.NewCommonEdgeX(errors.KindDuplicateName, fmt.Sprintf(http.DeviceCodeAlreadyExist, device.Code), err)
	}

	//检查产品存不存在
	exists, err = productIdExists(conn, device.ProductId)
	if err != nil {
		return d, errors.NewCommonEdgeXWrapper(err)
	} else if !exists {
		return d, errors.NewCommonEdgeX(errors.KindInvalidId, http.ProductNotExist, err)
	}

	ts := pkgCommon.MakeTimestamp()
	if device.Created == 0 {
		device.Created = ts
	}
	device.Modified = ts

	uid, _ := uuid.NewUUID()
	device.Id = uid.String()

	registerTime := time.Now().Format(utils.TimeFormat)
	device.RegisterTime = registerTime

	storedKey := deviceStoredKey(device.Id)

	dp, err := generateDeviceProfile(conn, device)
	if err != nil {
		return d, errors.NewCommonEdgeXWrapper(err)
	}

	pid, err := deviceProfileAdd(conn, device.Id, dp)
	if err != nil {
		return d, errors.NewCommonEdgeXWrapper(err)
	}

	device.ProfileId = pid
	device.ProfileName = dp.Name
	device.Name = fmt.Sprintf("device%s", device.Id)

	transports, err := generateTransportsFromProduct(conn, device)
	err = transportAdd(conn, device.Id, transports...)
	if err != nil {
		return d, errors.NewCommonEdgeXWrapper(err)
	}

	_ = conn.Send(MULTI)
	err = sendAddDeviceCmd(conn, storedKey, device)
	if err != nil {
		return d, errors.NewCommonEdgeXWrapper(err)
	}
	_, errRedis := conn.Do(EXEC)
	if errRedis != nil {
		err = errors.NewCommonEdgeX(errors.KindDatabaseError, "device creation failed", err)
	}

	return device, err
}

// sendAddDeviceCmd send redis command for adding device
func sendAddDeviceCmd(conn redis.Conn, storedKey string, d models.Device) errors.EdgeX {

	dsJSONBytes, err := json.Marshal(d)
	if err != nil {
		return errors.NewCommonEdgeX(errors.KindContractInvalid, "unable to JSON marshal device for Redis persistence", err)
	}

	_ = conn.Send(SET, storedKey, dsJSONBytes)
	_ = conn.Send(ZADD, DeviceCollection, time.Now().UnixMilli(), storedKey)
	_ = conn.Send(HSET, edgexRedis.DeviceCollectionName, d.Name, storedKey)
	_ = conn.Send(ZADD, CreateKey(edgexRedis.DeviceCollectionServiceName, d.ServiceName), d.Modified, storedKey)
	_ = conn.Send(ZADD, CreateKey(edgexRedis.DeviceCollectionProfileName, d.ProfileName), d.Modified, storedKey)
	for _, label := range d.Labels {
		_ = conn.Send(ZADD, CreateKey(edgexRedis.DeviceCollectionLabel, label), d.Modified, storedKey)
	}

	codeFieldKey := CreateKey(DeviceCodeCollection, d.Code)
	codeValueKey := CreateKey(DeviceCollection, d.Id)
	_ = conn.Send(HSET, DeviceCodeCollection, codeFieldKey, codeValueKey)

	codeNameKey := CreateKey(DeviceCodeNameCollection, d.Code, d.DisplayName)
	_ = conn.Send(HSET, DeviceNameSearchCollection, codeNameKey, codeValueKey)
	_ = conn.Send(ZADD, DeviceCodeNameCollection, time.Now().UnixMilli(), codeNameKey)

	productDeviceKey := CreateKey(ProductDeviceCollection, d.ProductId)
	_ = conn.Send(ZADD, productDeviceKey, time.Now().UnixMilli(), codeValueKey)
	return nil
}

// generateDeviceProfile 生成设备元信息
func generateDeviceProfile(conn redis.Conn, d models.Device) (models.DeviceProfile, errors.EdgeX) {
	dp, err := mock.GetMockDeviceProfile()
	if err != nil {
		return models.DeviceProfile{}, errors.NewCommonEdgeXWrapper(err)
	}
	dp.Id, _ = utils.GenerateUUID()
	dp.Name = fmt.Sprintf("DeviceProfile%s", dp.Id)
	dp.Labels = d.Labels
	ts := pkgCommon.MakeTimestamp()
	dp.Model = fmt.Sprintf("%s&%d", dp.Model, ts)
	dp.Description = fmt.Sprintf("device %s's profile", d.Name)

	err = generateCommandAndResources(conn, d.ProductId, &dp)
	if err != nil {
		return models.DeviceProfile{}, errors.NewCommonEdgeXWrapper(err)
	}
	return dp, nil
}

// generateCommandAndResources 生成设备功能和属性
func generateCommandAndResources(conn redis.Conn, productId string, deviceProfile *models.DeviceProfile) errors.EdgeX {
	attributes, err := AttributesByProductId(conn, 0, -1, productId)
	if err != nil {
		return errors.NewCommonEdgeX(errors.KindServerError, http.ProductNotEnabled, err)
	}
	functions, err := AllFunctionsByProductId(conn, 0, -1, productId)
	if err != nil {
		return errors.NewCommonEdgeX(errors.KindServerError, http.ProductNotEnabled, err)
	}

	resources := make([]models.DeviceResource, len(attributes))
	for index, attribute := range attributes {
		ar := attribute.Resource
		resource := models.DeviceResource{
			Code: ar.Code,
			DeviceResource: edgexModels.DeviceResource{
				Description: ar.Description,
				Name:        ar.Name,
				IsHidden:    ar.IsHidden,
				Tag:         ar.Tag,
				Attributes:  ar.Attributes,
				Properties:  edgexModels.ResourceProperties(ar.Properties),
			},
		}
		resources[index] = resource
	}

	commands := make([]models.DeviceCommand, len(functions))
	for index, function := range functions {
		fc := function.Command

		resourceOperations := make([]models.ResourceOperation, len(fc.ResourceOperations))
		for i, operation := range fc.ResourceOperations {
			res, err := ProductAttributeById(conn, operation.ProductResourceId)
			if err != nil {
				continue
			}
			if res.Resource.Code == "" {
				continue
			}
			resourceOperations[i] = models.ResourceOperation{
				ResourceCode: res.Resource.Code,
				ResourceOperation: edgexModels.ResourceOperation{
					DeviceResource: operation.ProductResource,
					DefaultValue:   operation.DefaultValue,
					Mappings:       operation.Mappings,
				},
			}
		}
		command := models.DeviceCommand{
			Code:        fc.Code,
			Description: fc.Description,
			DeviceCommand: edgexModels.DeviceCommand{
				Name:      fc.Name,
				IsHidden:  fc.IsHidden,
				ReadWrite: fc.ReadWrite,
			},
			ResourceOperations: resourceOperations,
		}
		commands[index] = command
	}

	deviceProfile.DeviceResources = resources
	deviceProfile.DeviceCommands = commands
	return nil
}

func generateTransportsFromProduct(conn redis.Conn, device models.Device) ([]models.Transport, errors.EdgeX) {
	transports, err := AllTransportsByProductId(conn, 0, -1, device.ProductId)
	if err != nil {
		return nil, errors.NewCommonEdgeX(errors.KindServerError, http.ProductNotEnabled, err)
	}

	deviceTransports := make([]models.Transport, len(transports))
	for index, transport := range transports {

		dt := models.Transport{
			Name:        transport.Name,
			Type:        transport.Type,
			Topic:       transport.Topic,
			Description: transport.Description,
		}
		deviceTransports[index] = dt
	}
	return deviceTransports, nil
}

// deviceResourceUpdate device update
func deviceUpdate(conn redis.Conn, device models.Device) errors.EdgeX {

	storedKey := CreateKey(DeviceCollection, device.Id)

	var origin models.Device
	err := getObjectById(conn, storedKey, &origin)
	if err != nil {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, http.DataBaseQueryError, err)
	}

	ts := pkgCommon.MakeTimestamp()
	origin.Modified = ts
	if serviceName := device.ServiceName; serviceName != "" {
		origin.ServiceName = serviceName
	}

	displayName := device.DisplayName
	if displayName != origin.DisplayName && displayName != "" {

		codeNameKeyOri := CreateKey(DeviceCodeNameCollection, origin.Code, origin.DisplayName)
		_ = conn.Send(HDEL, DeviceNameSearchCollection, codeNameKeyOri)
		_ = conn.Send(ZREM, DeviceCodeNameCollection, codeNameKeyOri)

		origin.DisplayName = displayName
		codeNameKey := CreateKey(DeviceCodeNameCollection, origin.Code, origin.DisplayName)
		_ = conn.Send(HSET, DeviceNameSearchCollection, codeNameKey, storedKey)
		_ = conn.Send(ZADD, DeviceCodeNameCollection, time.Now().UnixMilli(), codeNameKey)
	}
	origin.Labels = device.Labels
	origin.NodeType = device.NodeType
	origin.Description = device.Description
	origin.AdminState = device.AdminState
	origin.OperatingState = device.OperatingState
	origin.Protocols = device.Protocols

	//自动采集保存
	origin.AutoEvents = device.AutoEvents

	_ = deleteAllAutoEventByDeviceId(conn, device.Id)
	err = autoEventAdd(conn, device.Id, device.AutoEvents...)
	if err != nil {
		return err
	}

	dsJSONBytes, errJson := json.Marshal(origin)
	if errJson != nil {
		return errors.NewCommonEdgeX(errors.KindContractInvalid, "unable to JSON marshal device for Redis persistence", err)
	}
	_ = conn.Send(SET, storedKey, dsJSONBytes)
	return nil
}

// deviceDelete device delete
func deviceDelete(conn redis.Conn, deviceIds ...string) ([]models.Device, errors.EdgeX) {
	var devices []models.Device
	for _, deviceId := range deviceIds {
		_ = deviceProfileDelete(conn, deviceId)
		device, err := deleteDeviceById(conn, deviceId)
		if err != nil {
			devices = append(devices, *device)
		}
	}
	return devices, nil
}

// deleteDeviceById deletes the device by id
func deleteDeviceById(conn redis.Conn, id string) (*models.Device, errors.EdgeX) {
	device, err := deviceById(conn, id)
	if err != nil {
		return nil, errors.NewCommonEdgeXWrapper(err)
	}
	err = deleteDevice(conn, device)
	if err != nil {
		return nil, errors.NewCommonEdgeXWrapper(err)
	}

	codeFieldKey := CreateKey(DeviceCodeCollection, device.Code)
	_ = conn.Send(HDEL, DeviceCodeCollection, codeFieldKey)

	codeNameKey := CreateKey(DeviceCodeNameCollection, device.Code, device.Name)
	_ = conn.Send(HDEL, DeviceNameSearchCollection, codeNameKey)
	_ = conn.Send(ZREM, DeviceCodeNameCollection, codeNameKey)

	//删除产品设备关联
	storedKey := deviceStoredKey(device.Id)
	productDeviceKey := CreateKey(ProductDeviceCollection, device.ProductId)
	_ = conn.Send(ZREM, productDeviceKey, storedKey)
	return &device, nil
}

// deviceById query device by id from DB
func deviceById(conn redis.Conn, id string) (device models.Device, edgeXerr errors.EdgeX) {
	edgeXerr = getObjectById(conn, deviceStoredKey(id), &device)
	if edgeXerr != nil {
		return device, errors.NewCommonEdgeXWrapper(edgeXerr)
	}
	events, err := autoEventsGetByDeviceId(conn, id)
	if err != nil {
		return device, err
	}
	device.AutoEvents = events
	_ = fulfillProductInfoInDevice(conn, &device)
	return
}

// deleteDevice deletes a device
func deleteDevice(conn redis.Conn, device models.Device) errors.EdgeX {
	storedKey := deviceStoredKey(device.Id)
	_ = conn.Send(MULTI)
	sendDeleteDeviceCmd(conn, storedKey, device)
	_, err := conn.Do(EXEC)
	if err != nil {
		return errors.NewCommonEdgeX(errors.KindDatabaseError, "device deletion failed", err)
	}
	return nil
}

// sendDeleteDeviceCmd send redis command for deleting device
func sendDeleteDeviceCmd(conn redis.Conn, storedKey string, device models.Device) {
	_ = conn.Send(DEL, storedKey)
	_ = conn.Send(ZREM, DeviceCollection, storedKey)
	_ = conn.Send(HDEL, edgexRedis.DeviceCollectionName, device.Name)
	_ = conn.Send(ZREM, CreateKey(edgexRedis.DeviceCollectionServiceName, device.ServiceName), storedKey)
	_ = conn.Send(ZREM, CreateKey(edgexRedis.DeviceCollectionProfileName, device.ProfileName), storedKey)
	for _, label := range device.Labels {
		_ = conn.Send(ZREM, CreateKey(edgexRedis.DeviceCollectionLabel, label), storedKey)
	}
}

// 为设备添加产品名称
func fulfillProductInfoInDevices(conn redis.Conn, devices []models.Device) ([]models.Device, errors.EdgeX) {

	ds := make([]models.Device, len(devices))
	for index, device := range devices {
		_ = fulfillProductInfoInDevice(conn, &device)
		ds[index] = device
	}
	return ds, nil
}

// 为设备添加产品名称
func fulfillProductInfoInDevice(conn redis.Conn, device *models.Device) errors.EdgeX {
	product, _ := productById(conn, device.ProductId)
	device.ProductName = product.ProductName
	return nil
}

// 同步自动采集事件到设备详情
func syncAutoEventsToDevice(conn redis.Conn, deviceId string) errors.EdgeX {

	var device models.Device
	err := getObjectById(conn, deviceStoredKey(deviceId), &device)
	if err != nil {
		return errors.NewCommonEdgeXWrapper(err)
	}
	events, err := autoEventsGetByDeviceId(conn, deviceId)
	if err != nil {
		return err
	}
	device.AutoEvents = events

	dsJSONBytes, errJson := json.Marshal(device)
	if errJson != nil {
		return errors.NewCommonEdgeX(errors.KindContractInvalid, http.DataBaseQueryError, err)
	}

	storedKey := CreateKey(DeviceCollection, deviceId)
	_ = conn.Send(SET, storedKey, dsJSONBytes)
	return nil
}

func countByDeviceStatus(conn redis.Conn, validDuration int32) (models.DeviceCount, errors.EdgeX) {
	var deviceCount models.DeviceCount
	// get the total count of the lut sorted set
	storekey := CreateKey(DeviceCollection)
	total, err := getMemberNumber(conn, ZCARD, storekey)
	if err != nil {
		return deviceCount, errors.NewCommonEdgeXWrapper(err)
	}

	ts := pkgCommon.MakeTimestamp()
	min := ts - int64(validDuration)

	online, err := getMemberCountByScoreRange(conn, DeviceLastUpdateTime, int(min), int(ts))

	deviceCount = models.DeviceCount{
		Total:   total,
		Online:  online,
		Offline: total - online,
	}

	_ = removeMembersBySomeRange(conn, DeviceLastUpdateTime, 0, int32(min))
	return deviceCount, err
}
